Skip to content

Repository files navigation

██╗     ██╗███████╗██████╗ ███████╗██╗  ██╗
██║     ██║██╔════╝██╔══██╗██╔════╝╚██╗██╔╝
██║     ██║███████╗██║  ██║█████╗   ╚███╔╝
██║     ██║╚════██║██║  ██║██╔══╝   ██╔██╗
███████╗██║███████║██████╔╝███████╗██╔╝ ██╗
╚══════╝╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝  ╚═╝

Cybersecurity Projects Rust Keyless AFL++ Ground truth License: AGPLv3

A keyless memory-corruption scanner for C and C++ parsers in Rust. Point it at a source tree and it ranks which functions actually parse untrusted bytes, writes a fuzz harness for the best one with no model in the loop, builds the library under AddressSanitizer without a hand-written build script, runs an AFL++ campaign, then collapses the crash pile into distinct bugs with a stack-hash key, hand-labelled severity, a minimized reproducer proven to still trigger the same bug, and a coordinated-disclosure advisory in human and machine form. It needs no API key, no account and no cloud, and every stage refuses out loud rather than guessing.

Why the parsers keep failing

Memory corruption in file-format and media parsers is the CVE class that refuses to die. These libraries are old C, they run on bytes an attacker fully controls, and they sit underneath everything: an image decoder is reachable from a browser tab, a chat app, a thumbnailer and a mail client at the same time. CVE-2023-4863, a heap buffer overflow in libwebp's Huffman decoder, was exploited in the wild and forced emergency out-of-band patches across Chrome, Firefox, Android and Electron in September 2023, because a single parser is a shared dependency of half the internet. Stagefright did the same to Android in 2015 through the media parser, reachable by MMS.

Fuzzing is how these get found, and fuzzing itself is a solved commodity. AFL++ is free, fast and excellent. The expensive parts are the two on either side of it. Somebody has to decide which of nine hundred exported functions takes attacker bytes and write a harness that drives it correctly, and somebody has to turn the resulting pile of thousands of crash files into the handful of distinct bugs it actually represents, with a reproducer a maintainer will accept. Both are hand work, both need someone who already knows the library, and both are why most C projects have no fuzz target at all.

lisdex automates the two ends and drives AFL++ in the middle. It is validated against Magma, a benchmark of real historical CVEs re-planted into real libraries with canaries that report which specific bug a crash reached, so "it found something" can be checked against ground truth rather than asserted.

What it does

source tree -> entry points -> harness -> instrumented build -> campaign
            -> crash pile -> distinct bugs -> severity -> advisory + reproducer

A real campaign against libpng: 182 crash files, 2 distinct bugs. Getting from the first number to the second is most of what this tool is for.

Quick Start

curl -fsSL https://angelamos.com/lisdex/install.sh | bash

One command, zero further steps: it installs the Rust toolchain and AFL++ if they are missing, builds lisdex, drops it on your PATH, and leaves lisdex runnable by name. No API key, no account, no cloud. Then point it at a tree:

# what in this tree parses untrusted bytes?
lisdex surface ./libpng

# build it under AddressSanitizer with no hand-written build script
lisdex build https://github.com/glennrp/libpng --work /tmp/lisdex

# fuzz it
lisdex fuzz --target /tmp/lisdex/builds/libpng/afl/harness \
            --seeds ./corpus --findings /tmp/out --db /tmp/lisdex.sqlite

# watch it
lisdex tui   --db /tmp/lisdex.sqlite --findings /tmp/out
lisdex serve --db /tmp/lisdex.sqlite --findings /tmp/out

# what did it find, and how bad?
lisdex crashes --db /tmp/lisdex.sqlite --campaign 1

# draft the disclosure
lisdex bundle --db /tmp/lisdex.sqlite --campaign 1 --out /tmp/bundles

lisdex bundle writes one directory per distinct bug: the advisory in human and machine form, the exact crashing bytes, the sanitizer report, and an executable reproduce.sh.

Commands

Command What it does
surface Rank the parser entry points of a source tree
harness Synthesize a harness for a ranked entry point, with no LLM
verify Run the four harness oracles against one harness source file
build Auto-build a target under sanitizers
fuzz Run an AFL++ campaign
coverage Report the coverage curve a campaign recorded
corpus Distil a corpus to the smallest set reaching the same edges
crashes List the crashes persisted for a campaign
import Persist the crashes of a findings directory a killed campaign left behind
minimize Minimize one reproducer per distinct bug
advisory Draft a coordinated-disclosure advisory per distinct bug
bundle Write a self-contained reproducer bundle per distinct bug
tui Terminal dashboard over a live campaign
serve Local web console over the same state
bench Compare two builds over repeated trials, with a Mann-Whitney U test
rank-eval Score the cold-start ranking against the entry points real harnesses call
llm Report which optional booster tier is configured

What it refuses to do

These are deliberate, and they are the reason to trust the rest of the output.

  • It will not key a crash it cannot symbolize. An unsymbolized frame is a module plus an offset, shared by every crash of that kind in that module. Bucketing on it merges unrelated bugs. lisdex reports the crash unkeyed instead.
  • It will not guess an affected version range, a CVSS score, or attacker reachability. A campaign fuzzed one build. Those fields are required keys typed string-or-null, and both CVE and GHSA accept an unknown.
  • It will not emit a harness that fuzzes handle plumbing. If an entry point needs an opaque context lisdex cannot build, it refuses and says which parameter blocked it.
  • It will not report a planted bug. Crashes from a benchmark-instrumented build are stamped NOT DISCLOSABLE with no filing route.

The optional booster

lisdex works fully keyless. A local model can be attached as an optional booster, and it only ever proposes an answer that lisdex's own oracles then accept or refuse.

just ollama-up
just ollama-model     # qwen2.5-coder:14b-instruct-q6_K, Apache-2.0
lisdex llm --ask

There is no degraded keyless mode. The booster is an accelerator, never a requirement.

Requirements

Linux, a C compiler, and AFL++. The installer handles all three, including libclang, which lisdex surface loads at runtime.

lisdex drives AFL++ as a separate process and does not link or vendor it. AFL++ is AGPL-3.0-or-later; lisdex is AGPL-3.0; the installer fetches AFL++ upstream and unmodified.

Learn

The learn/ folder is the long version.

Development

just dev-up        # the toolchain container
just dev-verify    # prove AFL++, ASAN, cmplog and the LTO pass all work
just test          # the workspace suite
just check         # fmt, clippy -D warnings, tests
just magma-libpng  # build the Magma validation rig
just m9-gate       # one milestone's exit gate, end to end

Everything runs in Docker. The exit gates run real campaigns against a Magma-instrumented libpng and check the result against canary ground truth.

License

AGPL-3.0.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages